# -*- coding: utf-8 -*-

'''A group of Line objects.
'''
'''Line对象的组。
'''


import string
from .Line import Line
from .TextSpan import TextSpan
from ..image.ImageSpan import ImageSpan
from ..common.Collection import ElementCollection
from ..common.share import TextAlignment
from ..common import constants
from ..common.share import is_list_item


class Lines(ElementCollection):
    '''Collection of text lines.'''
    '''文本行的集合。'''

    @property
    def unique_parent(self):
        '''Whether all contained lines have same parent.'''
        '''所有包含的行是否具有相同的父级。'''
        if not bool(self): return False

        first_line = self._instances[0]
        return all(line.same_source_parent(first_line) for line in self._instances)


    def restore(self, raws:list):
        '''Construct lines from raw dicts list.'''
        '''从原始字典列表构造行。'''
        for raw in raws:
            line = Line(raw)
            self.append(line)
        return self


    def text(self):
        '''For debugging.'''
        '''用于调试。'''
        return '\n'.join([line.text for line in self])


    @property
    def image_spans(self):
        '''Get all ImageSpan instances.'''
        '''获取所有ImageSpan实例。'''
        spans = []
        for line in self._instances:
            spans.extend(line.image_spans)
        return spans

    
    def split_vertically_by_text(self, line_break_free_space_ratio:float, new_paragraph_free_space_ratio:float):
        '''Split lines into separate paragraph by checking text. The parent text block consists of 
        lines with similar line spacing, while lines in other paragraph might be counted when the
        paragraph spacing is relatively small. So, it's necessary to split those lines by checking
        the text contents.

        .. note::
            Considered only normal reading direction, from left to right, from top
            to bottom.
        '''
        '''通过检查文本将行分成单独的段落。父文本块由具有相似行间距的行组成，
        而当段落间距相对较小时，可能会计入其他段落中的行。因此，有必要通过检查
        文本内容来分割这些行。

        .. 注意::
            仅考虑正常阅读方向，从左到右，从上到下。
        '''
        rows = self.group_by_physical_rows()

        for row in rows:
            row.sort_in_line_order() # sort lines in row
                                    # 对行中的线条进行排序

        # skip if only one row
        # 如果只有一行，则跳过
        num = len(rows)
        if num==1: return rows

        # standard row width with first row excluded, considering potential indentation of fist line
        # 标准行宽度，不包括第一行，考虑第一行的潜在缩进
        W = max(row[-1].bbox[2]-row[0].bbox[0] for row in rows[1:])
        H = sum(row[0].bbox[3]-row[0].bbox[1] for row in rows) / num

        # check row by row
        # 逐行检查
        res = []
        lines = Lines()
        punc = tuple(constants.SENTENCE_END_PUNC)
        start_of_para = end_of_para = False # start/end of paragraph
                                          # 段落的开始/结束
        start_of_sen = end_of_sen = False   # start/end of sentence
                                           # 句子的开始/结束
        for row in rows:
            end_of_sen = row[-1].text.strip().endswith(punc)
            w =  row[-1].bbox[2]-row[0].bbox[0]

            if 0 and is_list_item(row[0].text[0]):
                # Treat bullet list items as separate paragraphs.
                # 将项目符号列表项作为单独的段落处理。
                start_of_para = True
            
            # end of a sentense and free space at the end -> end of paragraph
            # 句子结束并且末尾有空白空间 -> 段落结束
            elif end_of_sen and w/W <= 1.0-line_break_free_space_ratio:
                end_of_para = True

            # start of sentence and free space at the start -> start of paragraph
            # 句子开始并且开头有空白空间 -> 段落开始
            elif start_of_sen and (W-w)/H >= new_paragraph_free_space_ratio:
                start_of_para = True

            # take action
            # 采取行动
            if end_of_para:
                lines.extend(row)
                res.append(lines)
                lines = Lines()
            elif start_of_para:
                res.append(lines)
                lines = Lines()
                lines.extend(row)
            else:
                lines.extend(row)

            # for next round
            # 为下一轮准备
            start_of_sen = end_of_sen
            start_of_para = end_of_para = False
        
        # close the action
        # 结束操作
        if lines: res.append(lines)

        return res


    def adjust_last_word(self, delete_end_line_hyphen:bool):
        '''Adjust word at the end of line:
        # - it might miss blank between words from adjacent lines
        # - it's optional to delete hyphen since it might not at the the end 
           of line after conversion
        '''
        '''调整行尾的单词：
        # - 可能会缺少相邻行单词之间的空格
        # - it's optional to delete hyphen since it might not at the the end 
           of line after conversion
        '''
        punc_ex_hyphen = ''.join(c for c in string.punctuation if c!='-')
        def is_end_of_english_word(c):
            return c.encode().isalnum() or (c and c in punc_ex_hyphen)
        
        for i, line in enumerate(self._instances[:-1]):
            # last char in this line
            # 此行中的最后一个字符
            end_span = line.spans[-1]
            if not isinstance(end_span, TextSpan): continue
            end_chars = end_span.chars
            if not end_chars: continue 
            end_char = end_chars[-1]

            # first char in next line
            # 下一行中的第一个字符
            start_span = self._instances[i+1].spans[0]
            if not isinstance(start_span, TextSpan): continue
            start_chars = start_span.chars
            if not start_chars: continue 
            next_start_char = start_chars[0]            

            # delete hyphen if next line starts with lower case letter
            # 如果下一行以小写字母开头，则删除连字符
            if delete_end_line_hyphen and \
                end_char.c.endswith('-') and next_start_char.c.islower(): 
                end_char.c = '' # delete hyphen in a tricky way
                               # 以一种巧妙的方式删除连字符


            # add a space if both the last char and the first char in next line are alphabet,  
            # number, or English punctuation (excepting hyphen)
            # 如果下一行中的最后一个字符和第一个字符都是字母，
            # 数字或英文标点符号（连字符除外），则添加空格
            if is_end_of_english_word(end_char.c) and is_end_of_english_word(next_start_char.c):
                end_char.c += ' ' # add blank in a tricky way
                                 # 以一种巧妙的方式添加空格


    def parse_text_format(self, shape):
        '''Parse text format with style represented by rectangle shape.
        
        Args:
            shape (Shape): Potential style shape applied on blocks.
        
        Returns:
            bool: Whether a valid text style.
        '''
        '''解析由矩形形状表示的样式的文本格式。
        
        参数:
            shape (Shape): 应用于块的潜在样式形状。
        
        返回:
            bool: 是否为有效的文本样式。
        '''
        flag = False

        for line in self._instances:
            # any intersection in this line?
            # 此行中有任何交集吗？
            expanded_bbox = line.get_expand_bbox(constants.MAJOR_DIST)
            if not shape.bbox.intersects(expanded_bbox): 
                if shape.bbox.y1 < line.bbox.y0: break # lines must be sorted in advance
                                                      # 行必须预先排序
                continue

            # yes, then try to split the spans in this line
            # 是的，然后尝试分割此行中的spans
            split_spans = []
            for span in line.spans: 
                # include image span directly
                # 直接包括图像span
                if isinstance(span, ImageSpan): split_spans.append(span)                   

                # split text span with the format rectangle: span-intersection-span
                # 使用格式矩形分割文本span：span-交集-span
                else:
                    spans = span.split(shape, line.is_horizontal_text)
                    split_spans.extend(spans)
                    flag = True
                                            
            # update line spans                
            # 更新行spans
            line.spans.reset(split_spans)

        return flag


    def parse_line_break(self, bbox, 
                line_break_width_ratio:float, 
                line_break_free_space_ratio:float):
        '''Whether hard break each line. 

        Args:
            bbox (Rect): bbox of parent layout, e.g. page or cell.
            line_break_width_ratio (float): user defined threshold, break line if smaller than this value.
            line_break_free_space_ratio (float): user defined threshold, break line if exceeds this value.
        '''
        '''是否强制换行每一行。

        Hard line break helps ensure paragraph structure, but pdf-based layout calculation may
        change in docx due to different rendering mechanism like font, spacing. For instance, when
        one paragraph row can't accommodate a Line, the hard break leads to an unnecessary empty row.
        Since we can't 100% ensure a same structure, it's better to focus on the content - add line
        break only when it's necessary to, e.g. short lines.
        硬换行有助于确保段落结构，但基于pdf的布局计算可能会
        由于字体、间距等不同的呈现机制，docx发生了变化。例如，当
        一个段落行不能容纳一行，硬断行会导致不必要的空行。
        因为我们不能100%保证相同的结构，所以最好关注内容添加行
        只有在必要时才中断，例如短行。
        '''

        block = self.parent        
        idx0, idx1 = (0, 2) if block.is_horizontal_text else (3, 1)
        block_width = abs(block.bbox[idx1]-block.bbox[idx0])
        layout_width = bbox[idx1] - bbox[idx0]

        # hard break if exceed the width ratio
        line_break = block_width/layout_width <= line_break_width_ratio

        # check by each physical row
        rows = self.group_by_physical_rows()
        for lines in rows:
            for line in lines: line.line_break = 0

            # check the end line depending on text alignment
            if block.alignment == TextAlignment.RIGHT:
                end_line = lines[0]
                free_space = abs(block.bbox[idx0]-end_line.bbox[idx0])
            else:
                end_line = lines[-1]
                free_space = abs(block.bbox[idx1]-end_line.bbox[idx1])
            
            if block.alignment == TextAlignment.CENTER: free_space *= 2 # two side space
            
            # break line if 
            # - width ratio lower than the threshold; or 
            # - free space exceeds the threshold
            if line_break or free_space/block_width > line_break_free_space_ratio:
                end_line.line_break = 1
        
        # no break for last row
        for line in rows[-1]: line.line_break = 0


    def parse_tab_stop(self, line_separate_threshold:float):
        '''Calculate tab stops for parent block and whether add TAB stop before each line. 

        Args:
            line_separate_threshold (float): Don't need a tab stop if the line gap less than this value.
        '''
        # set all tab stop positions for parent block
        # Note these values are relative to the left boundary of parent block
        block = self.parent        
        idx0, idx1 = (0, 2) if block.is_horizontal_text else (3, 1)
        fun = lambda line: round(abs(line.bbox[idx0]-block.bbox[idx0]), 1)
        all_pos = set(map(fun, self._instances))
        tab_stops = list(filter(lambda pos: pos>=constants.MINOR_DIST, all_pos))
        tab_stops.sort() # sort in order
        block.tab_stops = tab_stops

        # no tab stop need
        if not block.tab_stops: return

        def tab_position(pos): # tab stop index of given position
            # 0   T0  <pos>  T1    T2
            i = 0
            pos -= block.bbox[idx0]
            for t in tab_stops:
                if pos<t: break
                i += 1
            return i

        # otherwise, set tab stop option for each line
        # Note: it might need more than one tab stops
        # https://github.com/dothinking/pdf2docx/issues/157
        ref = block.bbox[idx0]
        for i, line in enumerate(self._instances):
            # left indentation implemented with tab
            distance = line.bbox[idx0] - ref
            if distance>line_separate_threshold:
                line.tab_stop = tab_position(line.bbox[idx0])-tab_position(ref)

            # update stop reference position
            if line==self._instances[-1]: break
            ref = line.bbox[idx1] if line.in_same_row(self._instances[i+1]) else block.bbox[idx0]
